package cn.com.infcn.httpclient.lessons2;

import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.ProxySelector;
import java.net.Socket;
import java.util.concurrent.TimeUnit;

import org.apache.http.HeaderElement;
import org.apache.http.HeaderElementIterator;
import org.apache.http.HttpClientConnection;
import org.apache.http.HttpEntity;
import org.apache.http.HttpException;
import org.apache.http.HttpHost;
import org.apache.http.HttpRequest;
import org.apache.http.HttpResponse;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.protocol.HttpClientContext;
import org.apache.http.conn.ConnectionKeepAliveStrategy;
import org.apache.http.conn.ConnectionRequest;
import org.apache.http.conn.HttpClientConnectionManager;
import org.apache.http.conn.routing.HttpRoute;
import org.apache.http.conn.routing.HttpRoutePlanner;
import org.apache.http.conn.socket.PlainConnectionSocketFactory;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.conn.BasicHttpClientConnectionManager;
import org.apache.http.impl.conn.DefaultProxyRoutePlanner;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.apache.http.impl.conn.SystemDefaultRoutePlanner;
import org.apache.http.message.BasicHeaderElementIterator;
import org.apache.http.protocol.HTTP;
import org.apache.http.protocol.HttpContext;
import org.apache.http.util.EntityUtils;

/**
 * 第二章 连接管理
 * 
 * 
 * 2.1.持久连接
 * 
 * 两个主机建立连接的过程是很复杂的一个过程，涉及到多个数据包的交换，并且也很耗时间。Http连接需要的三次握手开销很大，
 * 这一开销对于比较小的http消息来说更大。但是如果我们直接使用已经建立好的http连接，这样花费就比较小，吞吐率更大。
 * HTTP/1.1默认就支持Http连接复用
 * 。兼容HTTP/1.0的终端也可以通过声明来保持连接，实现连接复用。HTTP代理也可以在一定时间内保持连接不释放，
 * 方便后续向这个主机发送http请求。这种保持连接不释放的情况实际上是建立的持久连接。HttpClient也支持持久连接。
 * 
 * 2.2.HTTP连接路由
 * 
 * HttpClient既可以直接、又可以通过多个中转路由（hops）和目标服务器建立连接。HttpClient把路由分为三种plain（明文
 * ），tunneled（隧道）和layered（分层）。隧道连接中使用的多个中间代理被称作代理链。
 * 客户端直接连接到目标主机或者只通过了一个中间代理，这种就是Plain路由
 * 。客户端通过第一个代理建立连接，通过代理链tunnelling，这种情况就是Tunneled路由
 * 。不通过中间代理的路由不可能时tunneled路由。客户端在一个已经存在的连接上进行协议分层
 * ，这样建立起来的路由就是layered路由。协议只能在隧道—>目标主机，或者直接连接（没有代理），这两种链路上进行分层。
 * 
 * 2.2.1.路由计算
 * 
 * RouteInfo接口包含了数据包发送到目标主机过程中，经过的路由信息。HttpRoute类继承了RouteInfo接口，是RouteInfo的具体实现，
 * 这个类是不允许修改的
 * 。HttpTracker类也实现了RouteInfo接口，它是可变的，HttpClient会在内部使用这个类来探测到目标主机的剩余路由。
 * HttpRouteDirector是个辅助类，可以帮助计算数据包的下一步路由信息。这个类也是在HttpClient内部使用的。
 * HttpRoutePlanner接口可以用来表示基于http上下文情况下
 * ，客户端到服务器的路由计算策略。HttpClient有两个HttpRoutePlanner的实现类
 * 。SystemDefaultRoutePlanner这个类基于java
 * .net.ProxySelector，它默认使用jvm的代理配置信息，这个配置信息一般来自系统配置或者浏览器配置
 * 。DefaultProxyRoutePlanner这个类既不使用java本身的配置，也不使用系统或者浏览器的配置。它通常通过默认代理来计算路由信息。
 * 
 * 2.2.2. 安全的HTTP连接
 * 
 * 为了防止通过Http消息传递的信息不被未授权的第三方获取、截获，Http可以使用SSL/TLS协议来保证http传输安全，这个协议是当前使用最广的。
 * 当然也可以使用其他的加密技术。但是通常情况下，Http信息会在加密的SSL/TLS连接上进行传输。
 * 
 * @author jjs
 *
 */
public class HttpClient2 {

	/**
	 * 2.3. HTTP连接管理器
	 * 
	 * 2.3.1. 管理连接和连接管理器
	 * 
	 * Http连接是复杂，有状态的，线程不安全的对象，所以它必须被妥善管理。一个Http连接在同一时间只能被一个线程访问。
	 * HttpClient使用一个叫做Http连接管理器的特殊实体类来管理Http连接
	 * ，这个实体类要实现HttpClientConnectionManager接口
	 * 。Http连接管理器在新建http连接时，作为工厂类;管理持久http连接的生命周期
	 * ;同步持久连接（确保线程安全，即一个http连接同一时间只能被一个线程访问
	 * ）。Http连接管理器和ManagedHttpClientConnection的实例类一起发挥作用
	 * ，ManagedHttpClientConnection实体类可以看做http连接的一个代理服务器
	 * ，管理着I/O操作。如果一个Http连接被释放或者被它的消费者明确表示要关闭
	 * ，那么底层的连接就会和它的代理进行分离，并且该连接会被交还给连接管理器。
	 * 这是，即使服务消费者仍然持有代理的引用，它也不能再执行I/O操作，或者更改Http连接的状态。
	 * 下面的代码展示了如何从连接管理器中取得一个http连接：
	 * 
	 * @throws ClientProtocolException
	 * @throws IOException
	 */
	public static void test1() throws Exception {
		HttpClientContext context = HttpClientContext.create();

		HttpClientConnectionManager connMrg = new BasicHttpClientConnectionManager();
		HttpRoute route = new HttpRoute(new HttpHost("localhost", 80));
		// 获取新的连接，这里可能耗时很长时间
		ConnectionRequest connRequest = connMrg.requestConnection(route, null);

		// 10秒超时
		HttpClientConnection conn = connRequest.get(10, TimeUnit.SECONDS);
		try {
			// 如果连接失败
			if (!conn.isOpen()) {
				// establish connection based on its route info
				connMrg.connect(conn, route, 1000, context);
				// and mark it as route complete
				connMrg.routeComplete(conn, route, context);
			}
			// 处理自己相关的逻辑

		} catch (Exception e) {
			// TODO: handle exception
		} finally {
			connMrg.releaseConnection(conn, null, 1, TimeUnit.MINUTES);
		}
	}

	/**
	 * 2.3.2.简单连接管理器
	 * 
	 * BasicHttpClientConnectionManager是个简单的连接管理器，它一次只能管理一个连接。尽管这个类是线程安全的，
	 * 它在同一时间也只能被一个线程使用
	 * 。BasicHttpClientConnectionManager会尽量重用旧的连接来发送后续的请求，并且使用相同的路由
	 * 。如果后续请求的路由和旧连接中的路由不匹配
	 * ，BasicHttpClientConnectionManager就会关闭当前连接，使用请求中的路由重新建立连接
	 * 。如果当前的连接正在被占用，会抛出java.lang.IllegalStateException异常。
	 * 
	 * 2.3.3.连接池管理器
	 * 
	 * 相对BasicHttpClientConnectionManager来说，
	 * PoolingHttpClientConnectionManager是个更复杂的类
	 * ，它管理着连接池，可以同时为很多线程提供http连接请求。Connections are pooled on a per route
	 * basis.当请求一个新的连接时，如果连接池有有可用的持久连接，连接管理器就会使用其中的一个，而不是再创建一个新的连接。
	 * PoolingHttpClientConnectionManager维护的连接数在每个路由基础和总数上都有限制
	 * 。默认，每个路由基础上的连接不超过2个，总连接数不能超过20。在实际应用中，这个限制可能会太小了，尤其是当服务器也使用Http协议时。
	 * 下面的例子演示了如果调整连接池的参数：
	 * 
	 * @throws ClientProtocolException
	 * @throws IOException
	 */
	public static void test2() throws Exception {
		PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager();
		// increase max total connection to 200
		cm.setMaxTotal(200);
		// Increase default max connection per route to 20
		cm.setDefaultMaxPerRoute(20);
		// Increase max connections for localhost:80 to 50
		HttpHost localhost = new HttpHost("localhost", 80);

		cm.setMaxPerRoute(new HttpRoute(localhost), 50);

		CloseableHttpClient httpClient = HttpClients.custom().setConnectionManager(cm).build();
		// 相应逻辑
		httpClient.close();
	}

	/**
	 * 2.4.多线程请求执行
	 * 
	 * 当使用了请求连接池管理器（比如PoolingClientConnectionManager）后，HttpClient就可以同时执行多个线程的请求了
	 * 。 PoolingClientConnectionManager会根据它的配置来分配请求连接。如果连接池中的所有连接都被占用了，
	 * 那么后续的请求就会被阻塞
	 * ，直到有连接被释放回连接池中。为了防止永远阻塞的情况发生，我们可以把http.conn-manager.timeout的值设置成一个整数
	 * 。如果在超时时间内，没有可用连接，就会抛出ConnectionPoolTimeoutException异常。
	 * 
	 * @throws ClientProtocolException
	 * @throws IOException
	 * @throws InterruptedException
	 */
	public static void test3() throws ClientProtocolException, IOException, InterruptedException {
		PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager();
		CloseableHttpClient httpClient = HttpClients.custom().setConnectionManager(cm).build();

		String[] urisToGet = { "http://localhost:8080/httpclient-web/index.jsp",
				"http://localhost:8080/httpclient-web/sucess.jsp", "http://localhost:8080/httpclient-web/fail.jsp",
				"http://localhost:8080/httpclient-web/login.jsp", };

		GetThread[] threads = new GetThread[urisToGet.length];
		for (int i = 0; i < threads.length; i++) {
			HttpGet httpGet = new HttpGet(urisToGet[i]);
			threads[i] = new GetThread(httpClient, httpGet);
		}

		for (int j = 0; j < threads.length; j++) {
			threads[j].start();
		}

		for (int k = 0; k < threads.length; k++) {
			threads[k].join();
		}

		/**
		 * 2.5. 连接回收策略
		 * 
		 * 经典阻塞I/O模型的一个主要缺点就是只有当组侧I/O时，socket才能对I/O事件做出反应。当连接被管理器收回后，这个连接仍然存活，
		 * 但是却无法监控socket的状态
		 * ，也无法对I/O事件做出反馈。如果连接被服务器端关闭了，客户端监测不到连接的状态变化（也就无法根据连接状态的变化
		 * ，关闭本地的socket）。
		 * HttpClient为了缓解这一问题造成的影响，会在使用某个连接前，监测这个连接是否已经过时，如果服务器端关闭了连接
		 * ，那么连接就会失效。这种过时检查并不是100%有效，并且会给每个请求增加10到30毫秒额外开销。唯一一个可行的，且does not
		 * involve a one thread per socket model for idle
		 * connections的解决办法，是建立一个监控线程，来专门回收由于长时间不活动而被判定为失效的连接。
		 * 这个监控线程可以周期性的调用ClientConnectionManager类的closeExpiredConnections
		 * ()方法来关闭过期的连接，回收连接池中被关闭的连接。
		 * 它也可以选择性的调用ClientConnectionManager类的closeIdleConnections
		 * ()方法来关闭一段时间内不活动的连接。
		 */
		// 监控线程是否失效，空闲或超时
		IdleConnectionMonitorThread idleConnectionMonitorThread = new IdleConnectionMonitorThread(cm);
		idleConnectionMonitorThread.start();
		idleConnectionMonitorThread.shutdown();
	}

	/**
	 * 2.6. 连接存活策略
	 * 
	 * Http规范没有规定一个持久连接应该保持存活多久。有些Http服务器使用非标准的Keep-Alive头消息和客户端进行交互，
	 * 服务器端会保持数秒时间内保持连接。HttpClient也会利用这个头消息。如果服务器返回的响应中没有包含Keep-Alive头消息，
	 * HttpClient会认为这个连接可以永远保持
	 * 。然而，很多服务器都会在不通知客户端的情况下，关闭一定时间内不活动的连接，来节省服务器资源。在某些情况下默认的策略显得太乐观
	 * ，我们可能需要自定义连接存活策略。
	 * 
	 * @throws ClientProtocolException
	 * @throws IOException
	 */
	public static void test4() throws ClientProtocolException, IOException {
		ConnectionKeepAliveStrategy myStrategy = new ConnectionKeepAliveStrategy() {
			@Override
			public long getKeepAliveDuration(HttpResponse response, HttpContext context) {
				// Honor 'keep-alive' header
				HeaderElementIterator it = new BasicHeaderElementIterator(response.headerIterator(HTTP.CONN_KEEP_ALIVE));
				while (it.hasNext()) {
					HeaderElement he = it.nextElement();
					String param = he.getName();
					String value = he.getValue();
					if (value != null && param.equalsIgnoreCase("timeout")) {
						try {
							return Long.parseLong(value) * 1000;
						} catch (NumberFormatException e) {

						}
					}
				}

				HttpHost target = (HttpHost) context.getAttribute(HttpClientContext.HTTP_TARGET_HOST);
				if ("www.naughty-server.com".equalsIgnoreCase(target.getHostName())) {
					// keep alive for 5 sconds only
					return 5 * 1000;
				} else {
					// otherwise keep alive for 30 seconds
					return 30 * 1000;
				}

			}
		};

		CloseableHttpClient client = HttpClients.custom().setKeepAliveStrategy(myStrategy).build();
		client.close();
	}

	public static void test5() throws ClientProtocolException, IOException {

		HttpClientContext clientContext = HttpClientContext.create();
		PlainConnectionSocketFactory sf = PlainConnectionSocketFactory.getSocketFactory();
		Socket socket = sf.createSocket(clientContext);
		int timeout = 1000; // ms
		HttpHost target = new HttpHost("localhost");
		InetSocketAddress remoteAddress = new InetSocketAddress(InetAddress.getByAddress(new byte[] { 127, 0, 0, 1 }),
				80);
		socket = sf.connectSocket(timeout, socket, target, remoteAddress, null, clientContext);
	}

	/**
	 * 2.8. HttpClient代理服务器配置
	 * 
	 * 尽管，HttpClient支持复杂的路由方案和代理链，它同样也支持直接连接或者只通过一跳的连接。
	 * 使用代理服务器最简单的方式就是，指定一个默认的proxy参数。
	 * 
	 * @throws ClientProtocolException
	 * @throws IOException
	 */
	public static void test6() throws ClientProtocolException, IOException {

		HttpHost proxy = new HttpHost("someproxy", 8080);
		DefaultProxyRoutePlanner routePlanner1 = new DefaultProxyRoutePlanner(proxy);
		CloseableHttpClient httpclient1 = HttpClients.custom().setRoutePlanner(routePlanner1).build();
		
		/* 我们也可以让HttpClient去使用jre的代理服务器。 */
		SystemDefaultRoutePlanner routePlanner2 = new SystemDefaultRoutePlanner(ProxySelector.getDefault());
		CloseableHttpClient httpclient2 = HttpClients.custom().setRoutePlanner(routePlanner2).build();
		
		/* 又或者，我们也可以手动配置RoutePlanner，这样就可以完全控制Http路由的过程。 */
		HttpRoutePlanner routePlanner3 = new HttpRoutePlanner() {
			public HttpRoute determineRoute(HttpHost target, HttpRequest request, HttpContext context)
					throws HttpException {
				return new HttpRoute(target, null, new HttpHost("someproxy", 8080), "https".equalsIgnoreCase(target
						.getSchemeName()));
			}

		};
		CloseableHttpClient httpclient3 = HttpClients.custom().setRoutePlanner(routePlanner3).build();
	}

	public static void main(String[] args) throws Exception {

		// test1();
		test2();
		test3();
		test4();
		test5();
		test6();
	}
}

class GetThread extends Thread {
	private final CloseableHttpClient httpClient;
	private final HttpContext context;
	private final HttpGet httpGet;

	public GetThread(CloseableHttpClient httpClient, HttpGet httpGet) {
		super();
		this.httpClient = httpClient;
		this.httpGet = httpGet;
		this.context = HttpClientContext.create();
	}

	@Override
	public void run() {
		try {
			CloseableHttpResponse respone = httpClient.execute(httpGet, context);
			HttpEntity entity = respone.getEntity();
			System.out.println(EntityUtils.toString(entity));
			System.out.println("===============================");
		} catch (Exception e) {
			// TODO: handle exception
		}
	}
}

class IdleConnectionMonitorThread extends Thread {
	private final HttpClientConnectionManager connMgr;
	private volatile boolean shutdown;

	public IdleConnectionMonitorThread(HttpClientConnectionManager connMgr) {
		super();
		this.connMgr = connMgr;
	}

	@Override
	public void run() {
		try {
			while (!shutdown) {
				synchronized (this) {
					wait(5000);
					// Close expired connections
					connMgr.closeExpiredConnections();
					// Optionally, close connections
					// that have been idle longer than 30 sec
					connMgr.closeIdleConnections(30, TimeUnit.SECONDS);
				}
			}
		} catch (InterruptedException e) {
			// terminate
		}
	}

	public void shutdown() {
		shutdown = true;
		synchronized (this) {
			notifyAll();
		}
	}
}
